/*
 * Copyright (c) 2015. For Intelligent Group.
 */

package com.intelligent.ispc.core.service.impl;

import com.google.common.collect.Lists;
import com.intelligent.ispc.common.mapper.BeanMapper;
import com.intelligent.ispc.common.utils.ApplicationException;
import com.intelligent.ispc.common.utils.Digests;
import com.intelligent.ispc.common.utils.Encodes;
import com.intelligent.ispc.common.utils.ExceptionMessages;
import com.intelligent.ispc.core.dto.CredentialsUserDto;
import com.intelligent.ispc.core.dto.ProjectDto;
import com.intelligent.ispc.core.dto.SecurityUserDto;
import com.intelligent.ispc.core.entity.Group;
import com.intelligent.ispc.core.entity.Project;
import com.intelligent.ispc.core.entity.User;
import com.intelligent.ispc.core.repository.jpa.UserDao;
import com.intelligent.ispc.core.service.SecurityService;
import org.apache.commons.lang3.StringUtils;
import org.apache.shiro.SecurityUtils;
import org.apache.shiro.codec.CodecException;
import org.apache.shiro.util.ByteSource;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.io.File;
import java.io.InputStream;
import java.util.*;

/**
 * Created by Peter.Li on 2015/10/10.
 */
@Service
public class SecurityServiceImpl implements SecurityService {

    private static Logger logger = LoggerFactory.getLogger(SecurityServiceImpl.class);

    @Autowired
    private UserDao userDao;

    @Override
    public CredentialsUserDto findByLoginName(String name) {
        if (StringUtils.isBlank(name)) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }

        CredentialsUserDto userDto = null;

        try {
            User user = userDao.findByLoginName(name);
            if (user == null) {
                throw new ApplicationException(ExceptionMessages.EXCEPTION_OBJECT_DOES_NOT_EXIST);
            }
            userDto = BeanMapper.map(user, CredentialsUserDto.class);

        } catch (Exception e) {
            throw new ApplicationException(e);
        }

        return userDto;
    }


    @Override
    public CredentialsUserDto findRefreshUser(String name) {
        if (StringUtils.isBlank(name)) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }

        CredentialsUserDto userDto = null;

        try {
            /*userCacheFactory.getCacheUser().invalidateAll();
            userDto = userCacheFactory.getCacheUser().get(name);*/
            User user = userDao.findByLoginName(name);
            if (user == null) {
                throw new ApplicationException(ExceptionMessages.EXCEPTION_OBJECT_DOES_NOT_EXIST);
            }
            userDto = BeanMapper.map(user, CredentialsUserDto.class);

        } catch (Exception e) {
            throw new ApplicationException(e);
        }

        return userDto;
    }

    @Override
    public boolean authentication(String name, String password) {
        if (StringUtils.isBlank(name)) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }
        User user = userDao.findByLoginName(name);

        if (user == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_OBJECT_DOES_NOT_EXIST);
        }

        logger.info("Authentication user:{}", name);

        byte[] tokenHashedCredentials = this.encryptPassword(user.getPassword(), user.getSalt());
        byte[] accountCredentials = this.encryptPassword(password, user.getSalt());
        if (this.equals(tokenHashedCredentials, accountCredentials)) {
            logger.info("Authenticated successful for {}", name);
            return true;
        }
        return false;
    }

    @Override
    public CredentialsUserDto login(String name, String password) {
        if (StringUtils.isBlank(name)) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }

        User user = userDao.findByLoginName(name);

        if (user == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_OBJECT_DOES_NOT_EXIST);
        }

        byte[] tokenHashedCredentials = this.encryptPassword(user.getPassword(), user.getSalt());
        byte[] accountCredentials = this.encryptPassword(password, user.getSalt());
        if (this.equals(tokenHashedCredentials, accountCredentials)) {
            user.setLastLoginTime(new Date());
            userDao.save(user);
            return BeanMapper.map(user, CredentialsUserDto.class);
        }

        return null;
    }

    @Override
    public CredentialsUserDto refreshLoginTime(String name) {
        if (StringUtils.isBlank(name)) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }

        User user = userDao.findByLoginName(name);

        if (user == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_OBJECT_DOES_NOT_EXIST);
        }

        user.setLastLoginTime(new Date());

        userDao.save(user);

        return BeanMapper.map(user, CredentialsUserDto.class);
    }

    @Override
    public String getCurrentUserLoginName() {
        SecurityUserDto user = (SecurityUserDto) SecurityUtils.getSubject().getPrincipal();

        return user.getLoginName();
    }

    @Override
    public CredentialsUserDto getCurrentUser() {
        String userNameString = this.getCurrentUserLoginName();

        return findByLoginName(userNameString);
    }

    private byte[] encryptPassword(String password, String salt) {
        byte[] s = Encodes.decodeHex(salt);
        return Digests.sha1(password.getBytes(), s, SecurityService.HASH_INTERATIONS);
    }

    private boolean equals(Object tokenCredentials, Object accountCredentials) {
        byte[] tokenBytes = this.toBytes(tokenCredentials);
        byte[] accountBytes = this.toBytes(accountCredentials);
        return Arrays.equals(tokenBytes, accountBytes);
    }

    private byte[] toBytes(Object o) {
        if (o == null) {
            String msg = "Argument for byte conversion cannot be null.";
            throw new IllegalArgumentException(msg);
        } else {
            return o instanceof byte[] ? (byte[]) ((byte[]) o) : (o instanceof ByteSource ? ((ByteSource) o).getBytes() : (o instanceof char[] ? toBytes((char[]) ((char[]) o)) : (o instanceof String ? toBytes((String) o) : (o instanceof File ? this.toBytes((File) o) : (o instanceof InputStream ? this.toBytes((InputStream) o) : this.objectToBytes(o))))));
        }
    }

    private byte[] objectToBytes(Object o) {
        String msg = "The " + this.getClass().getName() + " implementation only supports conversion to " + "byte[] if the source is of type byte[], char[], String, " + ByteSource.class.getName() + " File or InputStream.  The instance provided as a method " + "argument is of type [" + o.getClass().getName() + "].  If you would like to convert " + "this argument type to a byte[], you can 1) convert the argument to one of the supported types " + "yourself and then use that as the method argument or 2) subclass " + this.getClass().getName() + "and override the objectToBytes(Object o) method.";
        throw new CodecException(msg);
    }

    @Override
    public List<ProjectDto> findDataPermissionForUser(String userId) {
        if (userId == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_PARAMETER_INPUT_INVALID);
        }
        User user = userDao.findById(Long.valueOf(userId));
        if (user == null) {
            throw new ApplicationException(ExceptionMessages.EXCEPTION_OBJECT_DOES_NOT_EXIST);
        }

        List<ProjectDto> projectDtos = Lists.newArrayList();
        List<Group> groups = user.getGroups();
        for (Group group : groups) {
            List<Project> projects = group.getProjects();
            for (Project project : projects) {
                ProjectDto projectDto = new ProjectDto();
                BeanMapper.copy(project, projectDto);
                if (!projectDtos.contains(projectDto)) {
                    projectDtos.add(projectDto);
                }
            }
        }

        return projectDtos;
    }

    public void setUserDao(UserDao userDao) {
        this.userDao = userDao;
    }
}
